Single-page applications (SPAs) provide a seamless user experience by dynamically updating the content of a web page without requiring a full page reload. Knockout.js, with its powerful data-binding and MVVM (Model-View-ViewModel) architecture, is an excellent choice for building SPAs. Combined with RESTful APIs, you can create highly responsive and interactive web applications. This guide will walk you through building an SPA using Knockout.js and RESTful APIs.
Setting Up the Project
Create the Project Structure
knockout-spa/
├── index.html
├── app.js
├── styles.css
├── api/
│ ├── products.json
└── server.js
Create the HTML File
index.html
<!doctype html>
<html lang="en">
<head>
<!-- Meta tags for character set and viewport configuration -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Title of the webpage -->
<title>Bootstrap demo</title>
<!-- Link to Bootstrap CSS for styling -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- Link to Knockout.js library for MVVM pattern support -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"
integrity="sha512-vs7+jbztHoMto5Yd/yinM4/y2DOkPLt0fATcN+j+G4ANY2z4faIzZIOMkpBmWdcxt+596FemCh9M18NUJTZwvw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Link to jQuery library -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<!-- Link to external JavaScript file -->
<script src="app.js"></script>
</body>
</html>
Create the Knockout.js ViewModel
app.js
// ViewModel for the application
class AppViewModel {
constructor() {
var self = this;
// Observables to store data and state
self.response = ko.observable({}); // Observable to store the response data
self.currentPage = ko.observable(0); // Observable to track the current page
self.totalPages = ko.observable(0); // Observable to track the total number of pages
self.pagination = ko.observable({}); // Observable to store pagination data
self.isLoading = ko.observable(false); // Observable to track loading state
self.error = ko.observable(null); // Observable to store error message
self.pageNo = ko.observable(self.pagination().current_page ? self.pagination().current_page : 1);
self.pageNo.subscribe(function(newValue){
let url = 'https://api.artic.edu/api/v1/artworks?page='+ parseInt(newValue);
self.fetchData(url);
});
// Function to fetch data from the API
self.fetchData = async function (url = '') {
self.isLoading(true); // Set loading state to true
self.error(''); // Clear any previous errors
let _url = url || 'https://api.artic.edu/api/v1/artworks'; // Default URL
try {
// Simulate an asynchronous API call using fetch
let response = await fetch(_url);
if (!response.ok) {
// If response is not ok, throw an error
throw new Error('Network response was not ok ' + response.statusText);
}
let result = await response.json(); // Parse response as JSON
self.response(result); // Update the observable array with the fetched data
// Extract pagination data from the response and update observables accordingly
if (result.pagination) {
self.pagination(result.pagination);
}
} catch (err) {
// If an error occurs during fetch, set error message
self.error(err.message);
} finally {
// Set loading state to false regardless of success or failure
self.isLoading(false);
}
};
self.showDetails = function(link) {
// Add your code to handle details here
console.log("Link clicked:", link);
}
}
}
// Apply bindings
ko.applyBindings(new AppViewModel());
Create the Product Section Template
Modify index.html to include the product list template.
<!-- Main container for the content -->
<div class="container my-3">
<!-- Header for the section -->
<h2>Data from Server async</h2>
<!-- Button to fetch data -->
<button data-bind="click: fetchData('')" class="btn btn-secondary">Fetch Data</button>
<hr />
<div id="app">
<!-- Display loading state -->
<div data-bind="visible: isLoading">
<div class="d-flex justify-content-center fw-semibold fs-5 align-items-center">
<span class="me-2">Loading...</span>
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
<!-- Display error message if any -->
<div class="d-flex justify-content-center fw-semibold fs-5 align-items-center"
data-bind="visible: error, text: error" style="color: red;"></div>
<div class="my-3 d-flex justify-content-end align-items-baseline">
<form>
<div class="mx-3">
<input class="form-control" data-bind="value: $root.pageNo, valueUpdate: 'input'"
placeholder="page no" />
</div>
</form>
<!-- Pagination -->
<span class="fw-semibold me-3" data-bind="with: { pages : pagination()}">
<span data-bind="text: pages.current_page"></span>
<span class="mx-1">of</span>
<span class="" data-bind="text: pages.total_pages"></span>
</span>
<ul class="pagination "
data-bind="with: { pages : pagination(), prevUrl: pagination().prev_url, nextUrl: pagination().next_url, currentPage: pagination().current_page ? pagination().current_page : 1 }">
<!-- First Page -->
<li class="page-item" data-bind="css: { 'disabled': !(currentPage > 1) }">
<a class="page-link" href="#" data-bind="click: function() { $root.fetchData(''); }">
First
</a>
</li>
<!-- Previous Page -->
<li class="page-item" data-bind="css: { 'disabled': !(currentPage > 1) }">
<a class="page-link" href="#" data-bind="click: function() { $root.fetchData(prevUrl); },
enable: (currentPage > 1)">
Previous
</a>
</li>
<!-- Current Page -->
<li class="page-item active"><a class="page-link" href="#"
data-bind="text: currentPage ? currentPage : 1"></a></li>
<!-- Next Page -->
<li class="page-item" data-bind="css: { 'disabled': !(currentPage < pages.total_pages) }">
<a class="page-link" href="#" data-bind="click: function() { $root.fetchData(nextUrl); }"> Next </a> </li> <!-- Last Page --> <li class="page-item" data-bind="css: { 'disabled': !(currentPage < pages.total_pages) }"> <a class="page-link" href="#" data-bind="click: function() { $root.fetchData('https://api.artic.edu/api/v1/artworks?page='+pages.total_pages); }"> Last </a> </li> </ul> </div> <div class="" data-bind="if: (response() && response().data && response().data.length <= 0)"> <hr /> <div class="d-flex justify-content-center fw-bold"> <div class="text-center"> <p class=" fs-2 "> <span class="me-2">😕</span> No records were found </p> <!-- <p class="fw-normal">try somthing new keywords</p> --> </div> </div> </div> <!-- Display data if available --> <div class="row g-3 row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4" data-bind="visible: !isLoading(), foreach: response().data"> <!-- Column for each item --> <div class="col"> <!-- Card to display item details --> <div class="card h-100"> <!-- Item Image --> <img data-bind="if: (image_id && thumbnail), attr: { src: 'https://www.artic.edu/iiif/2/' + image_id + '/full/400,/0/default.jpg', alt: thumbnail ? thumbnail.alt_text : '' }" class="card-img-top" height="250"> <div class="card-body"> <!-- Card title bound to item's title --> <h5 class="card-title text-capitalize" data-bind="text: title"></h5> <!-- Card text bound to item's body --> <!-- <p class="card-text" data-bind="text: body"></p> --> </div> <div class="card-footer border-0 pt-0"> <!-- Footer with item id and a link --> <p class="m-0 d-flex justify-content-between"> <!-- Item id displayed --> <span class="fs-5 fw-bold" data-bind="text: id"></span> <!-- Link to comments page for the item --> <a class="" data-bind="attr: { 'data-link': api_link }, click: function() { $root.showDetails(api_link); }"> <!-- SVG icon inside the link --> <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" class="bi bi-arrow-right-circle-fill" viewBox="0 0 16 16"> <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0M4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5z" /> </svg> </a> </p> </div> </div> </div> </div> </div> </div>
Read more
Knockout.js: Building Dynamic Web Applications
Leave Comment